Design - Command Framework
Wednesday, March 22, 2023
10:34 AM
The OneMore add-in extends OneNote with many new commands. Just like most OneNote commands, the OneMore commands are available through a set of concise menus on the Home ribbon bar. They're also available through a number of other mechanisms. Commands generally have the following requirements.
Requirements
- Commands should be created and destroyed using a standard pattern
- Commands should be accessible from the ribbon, from hotkeys, and from the Command Palette
- Commands should be discoverable such that they can be enumerated and added to context menus
- Commands should have access to the Ribbon to update or add/remove controls, e.g. Favorites or Snippets
- Commands should be tracked through a most-recently-used list, MRU
- Commands should be fully describable such that the MRU can hand that to Replay and Palette functions
Command Use Cases |
Ultimately, the goal is to simplify how we add new commands and automate all of these use cases. In How to Add a New Command, it shows that this can be done in three easy steps:
- Implement the command, inheriting from the Command base class
- Implement the proxy method in the AddInCommands.cs file
- Add a new button control in the ribbon by declaring it in the Ribbon.xml file
Assumptions
- Controls in the ribbon.xml include the onAction attribute, specifying the name of a callback method. Every callback method must be an imperative member, defined in the AddIn scope
- Everything must run async and be thread-safe
- Each command invocation should be as isolated as possible and behave like a good neighbor
Models
Obviously, the AddIn class is the root of everything. It implements IDTExtensibility2 that declares add-in lifecycle handlers from which we can grab a reference to IRibbonControl and IRibbonExtensibility, the latter of which declares the GetCustomUI callback where our responsibility is to return XML describing the add-in extensions to the ribbon.
We leverage the IDTEExtensibility2.OnStartupComplete handler to instantiate our own CommandFactory and start up any internal services such as the Command Service, Reminder Service[LINK] and Navigation Service[LINK].
The AddIn class is divided into multiple C# files, one of which is AddInCommands.cs. This file contains callback methods, each bound to a ribbon control via its onAction property. By convention, each callback method is a very simple proxy that uses the CommandFactory to instantiate and invoke a single command.
Additionally, each callback method may be decorated with a [CommandAttribute] that declares the resource ID used to lookup the translation string for the command, key definitions that declare the default shortcut key sequence, and an optional category for grouping commands in the Settings dialog.
[Command("ribAddFootnoteButton_Label", Keys.Control | Keys.Alt | Keys.F, "ribReferencesMenu")]
public async Task AddFootnoteCmd(IRibbonControl control)
=> await factory.Run<AddFootnoteCommand>();
Command Class
All commands derive from the abstract base class Command. Created by CommandFactory, each instance inherits protected fields to access an ILogger, the IRibbonUI, and a reference to the factory, allowing commands to invoke other commands.
The abstract Execute method must be implemented by inheritors and encapsulates the business logic of the command.
Command |
|
CommandFactory Class
This factory class is responsible for instantiating new commands and initializing their context including ILogger and IRibbonUI references.
The Run method dispatches the command to a new thread, calls its Execute method, and if successful and not cancelled, records it in the MRU.
CommandFactory |
──────────────────────────────────────────────────────────────────────────────────────────────────
Command Use Cases PlantUML (Refresh)
@startuml Command Use Cases
scale max 590 width
skin rose
top to bottom direction
usecase (Command) as cmd #LightSkyBlue
usecase Hotkeys
usecase Replay
usecase Palette
usecase Ribbon
usecase "Context Menus" as menus
usecase Aliases
cmd --> Hotkeys
cmd --> Palette
cmd --> Ribbon
cmd --> Replay
cmd --> menus
cmd --> Aliases
@enduml
Command PlantUML (Refresh)
@startuml Command
skinparam defaultFontSize 9
class Command {
#logger : ILogger
#ribbon : IRibbonUI
#factory : CommandFactory
+get_IsCancelled : bool
+XElement GetReplayArguments()
+{abstract} Task Execute(params object[] args)
}
@enduml
CommandFactory PlantUML (Refresh)
@startuml CommandFactory
skinparam defaultFontSize 9
class CommandFactory {
+Task Invoke(string action, string[] arguments)
+Task ReplayLastAction()
+Task Run(params object[] args)
}
@enduml
#omwiki #omdeveloper #omdesign
© 2020 Steven M Cohn. All rights reserved.
Please consider a sponsorship or one-time donation to support ongoing development
Created with OneNote.